﻿using System.Globalization;
using System.Text;

namespace SharePoint.Orm.Caml {
    public enum CamlLogicalOps {
        And,
        Or,
    }

    public enum CamlComparisonOps {
        BeginsWith,
        Contains,
        Eq, Geq, Gt,
        IsNotNull, IsNull,
        Leq, Lt, Neq
    }

    /// <summary>
    /// Creates a caml query for searching. Builds the queries in a linear fashion.
    /// </summary>
    public class CamlBuilder {
        private const string CAML_CLAUSE_START = "<Where>";
        private const string CAML_CLAUSE_END = "</Where>";
        private const string CAML_CLAUSE_TEST = "<{0}>{1}</{0}>";
        private const string CAML_STATEMENT = "<FieldRef Name='{0}' /><Value Type='{1}'>{2}</Value>";
        private const string CAML_LOOKUP_STATEMENT = "<FieldRef Name='{0}' LookupId='TRUE' /><Value Type='Lookup'>{1}</Value>";
        private const string CAML_FIELDREF_STATEMENT = "<FieldRef Name='{0}'/>";
        private const string CAML_FIELDREF_ASCENDING_STATEMENT = "<FieldRef Name='{0}' Ascending='{1}' />";
        private const string CAML_ORDERBY_STATEMENT = "<OrderBy {1}>{0}</OrderBy>";
        private const string CAML_GROUPBY_STATEMENT = "<GroupBy {1}>{0}</GroupBy>";
        private readonly StringBuilder _camlString = new StringBuilder();
        private readonly StringBuilder _orderString = new StringBuilder();
        private readonly StringBuilder _groupString = new StringBuilder();
        private readonly StringBuilder _orderAttrString = new StringBuilder();
        private readonly StringBuilder _groupAttrString = new StringBuilder();
        private int _fieldCount;
        private bool _hasOperator; //Prevent operators from being stacked on top of each other i.e. <Or><And></And></Or>
        /// <summary>
        /// Appends a field and test to the internal query.
        /// </summary>
        /// <param name="fieldName">The name of the field reference</param>
        /// <param name="fieldType">The type of the field reference</param>
        /// <param name="fieldText">The value of the field reference</param>
        /// <param name="fieldComparison">The logical comparison operator for the field reference</param>
        public void AddField(string fieldName, string fieldType, string fieldText, CamlComparisonOps fieldComparison) {
            string fieldString = string.Format(CultureInfo.InvariantCulture, CAML_STATEMENT, fieldName, fieldType, fieldText);
            AddField(fieldComparison, fieldString);
        }
        public void AddLookup(string fieldName, int fieldId, CamlComparisonOps fieldComparison) {
            string fieldString = string.Format(CultureInfo.InvariantCulture, CAML_LOOKUP_STATEMENT, fieldName, fieldId);
            AddField(fieldComparison, fieldString);
        }
        public void AddUnaryField(string field, CamlComparisonOps fieldComparison) {
            string fieldString = string.Format(CultureInfo.InvariantCulture, CAML_FIELDREF_STATEMENT, field);
            AddField(fieldComparison, fieldString);
        }
        private void AddField(CamlComparisonOps fieldComparison, string fieldString) {
            _camlString.Append(string.Format(CultureInfo.InvariantCulture, CAML_CLAUSE_TEST, CompareOperatorString(fieldComparison), fieldString));
            _fieldCount++;
            _hasOperator = false;
        }
        /// <summary>
        /// Appends a logical operator to the beginning and end of the internal query.
        /// </summary>
        /// <param name="logicalOperator">The logical operator to append on the query</param>
        public void AddOperator(CamlLogicalOps logicalOperator) {
            if (_fieldCount > 1) {
                if (!_hasOperator) {
                    _camlString.Insert(0, "<" + LogicalOperatorString(logicalOperator) + ">");
                    _camlString.Append("</" + LogicalOperatorString(logicalOperator) + ">");
                    _hasOperator = true;
                }
            }
        }

        public void AddStatement(string statement) {
            if (string.IsNullOrEmpty(statement)) return;
            _camlString.Append(statement);
            _fieldCount++;
            _hasOperator = false;
        }

        private static string LogicalOperatorString(CamlLogicalOps ops) {
            switch (ops) {
                case CamlLogicalOps.And:
                    return "And";
                case CamlLogicalOps.Or:
                    return "Or";
                default:
                    return "";
            }
        }
        private static string CompareOperatorString(CamlComparisonOps ops) {
            switch (ops) {
                case CamlComparisonOps.BeginsWith:
                    return "BeginsWith";
                case CamlComparisonOps.Contains:
                    return "Contains";
                case CamlComparisonOps.Eq:
                    return "Eq";
                case CamlComparisonOps.Geq:
                    return "Geq";
                case CamlComparisonOps.Gt:
                    return "Gt";
                case CamlComparisonOps.IsNotNull:
                    return "IsNotNull";
                case CamlComparisonOps.IsNull:
                    return "IsNull";
                case CamlComparisonOps.Leq:
                    return "Leq";
                case CamlComparisonOps.Lt:
                    return "Lt";
                case CamlComparisonOps.Neq:
                    return "Neq";
                default:
                    return "";
            }
        }
        /// <summary>
        /// Inserts a custom query at any place in the internal query.
        /// </summary>
        /// <param name="position">Where the query will be inserted</param>
        /// <param name="query">The custom query to be added</param>
        public void InsertQuery(int position, string query) {
            _camlString.Insert(position, query);
        }
        /// <summary>
        /// Prepends a custom query onto the front of the internal query.
        /// </summary>
        /// <param name="query">The custom query to be prepended</param>
        public void PrependQuery(string query) {
            _camlString.Insert(0, query);
        }
        /// <summary>
        /// Appends a custom query onto the end of the internal query.
        /// </summary>
        /// <param name="query">The custom query to be appended</param>
        public void AppendQuery(string query) {
            _camlString.Append(query);
        }
        /// <summary>
        /// The number of fields in the internal query
        /// </summary>
        /// <returns>Length of the query</returns>
        public int FieldLength() {
            return _camlString.Length;
        }
        /// <summary>
        /// Clears the internal query
        /// </summary>
        public void Clear() {
            _fieldCount = 0;
            _camlString.Remove(0, _camlString.Length);
            _orderString.Remove(0, _orderString.Length);
            _groupString.Remove(0, _groupString.Length);
            _orderAttrString.Remove(0, _orderAttrString.Length);
            _groupAttrString.Remove(0, _groupAttrString.Length);
            _hasOperator = false;
        }

        /// <summary>
        /// Gets only the where part of the CAML statement, but without the Where element.  This is useful in conjunction with
        /// AddStatement, for combining multiple CamlBuilders.
        /// </summary>
        /// <returns></returns>
        public string GetCamlOnly() {
            return _camlString.ToString();
        }

        /// <summary>
        /// Override the ToString to return the full query.
        /// </summary>
        /// <returns>The full caml query</returns>
        public override string ToString() {
            string result = "";
            if (_camlString.Length > 0)
                result += CAML_CLAUSE_START + _camlString + CAML_CLAUSE_END;
            if (_orderString.Length > 0)
                result += string.Format(CAML_ORDERBY_STATEMENT, _orderString, _orderAttrString);
            if (_groupString.Length > 0)
                result += string.Format(CAML_GROUPBY_STATEMENT, _groupString, _groupAttrString);
            return result;
        }
        /// <summary>
        /// Appends an order by query to the main query.
        /// </summary>
        /// <param name="query"></param>
        /// <param name="attributes">Attributes for the order by clause</param>
        public void OrderBy(string query, params string[] attributes) {
            _orderAttrString.Append(string.Join(" ", attributes));
            _orderString.Append(query);
        }
        public void OrderByFieldRef(string fieldRef, bool ascending) {
            _orderString.Append(string.Format(CAML_FIELDREF_ASCENDING_STATEMENT, fieldRef, ascending ? "TRUE" : "FALSE"));
        }
        public void OrderByFieldRef(string fieldRef, params string[] attributes) {
            _orderAttrString.Append(string.Join(" ", attributes));
            _orderString.Append(string.Format(CAML_FIELDREF_STATEMENT, fieldRef));
        }
        public static string OrderBySingleFieldRef(string query, params string[] attributes) {
            return SingleFieldRef(CAML_ORDERBY_STATEMENT, query, attributes);
        }

        /// <summary>
        /// Appends a group by query to the main query.
        /// </summary>
        /// <param name="query"></param>
        /// <param name="attributes"></param>
        public void GroupBy(string query, params string[] attributes) {
            _groupAttrString.Append(string.Join(" ", attributes));
            _groupString.Append(query);
        }
        public void GroupByFieldRef(string fieldRef, params string[] attributes) {
            _groupAttrString.Append(string.Join(" ", attributes));
            _groupString.Append(string.Format(CAML_FIELDREF_STATEMENT, fieldRef));
        }
        public static string GroupBySingleFieldRef(string query, params string[] attributes) {
            return SingleFieldRef(CAML_GROUPBY_STATEMENT, query, attributes);
        }
        private static string SingleFieldRef(string camlOperation, string query, string[] attributes) {
            string fieldrefStatement = string.Format(CAML_FIELDREF_STATEMENT, query);
            string joinedAttributes = string.Join(" ", attributes);
            return string.Format(camlOperation, fieldrefStatement, joinedAttributes);
        }
    }
}